/* -*- mode: c++ -*- */
// Parser grammar for beancount 3.0 input syntax (Bison C++).

//- Options -------------------------------------------------------------------

%require "3.7"

%language "c++"
%define api.namespace {beancount::parser}
%define api.parser.class {Parser}
%define parse.error detailed
%define parse.trace

// %verbose, have to specify the output file.
// %token-table // Has no effect on C++ code generation.

%defines
%locations
%define api.filename.type "const std::string"

// Input state to parser.
%parse-param {::beancount::scanner::Scanner& scanner} {::beancount::parser::Builder& builder}

//- Code blocks ---------------------------------------------------------------

// Inserted in header, before anything else
%code requires {

#include "beancount/cparser/inter.pb.h"
#include "beancount/cparser/options.pb.h"
#include "beancount/cparser/ledger.h"
#include "beancount/ccore/data.pb.h"
#include "beancount/ccore/number.pb.h"

#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <fstream>
#include <list>
#include <memory>
#include <optional>
#include <string>

#include "absl/strings/string_view.h"
#include "absl/strings/str_join.h"
#include "absl/time/civil_time.h"
#include "absl/types/optional.h"
#include "google/protobuf/descriptor.pb.h"
#include "google/protobuf/text_format.h"
#include "decimal.hh"

namespace beancount {
namespace scanner { class Scanner; }
namespace parser { class Builder; }
namespace parser {

// Parse the contents of a file.
std::unique_ptr<Ledger> ParseFile(std::string_view filename);

// Parse the contents of a string.
std::unique_ptr<Ledger> ParseString(std::string_view input_string,
                                    std::string_view filename,
                                    int line_offset = 0,
                                    bool debug = false,
                                    bool decimal_use_triple = false);

// An intermediate data structure holding the partially parsed tags and links.
struct TagsLinks {
  std::vector<std::string> tags;
  std::vector<std::string> links;
};

}  // namespace parser
}  // namespace beancount
}  // %code requires

// Inserted in header, at the end.
%code provides {
}  // %code provides

//-----------------------------------------------------------------------------
// Inserted in implementation after including parser header.
%code top {

#include "beancount/cparser/scanner.h"  /* Generated by reflex. */
#include "beancount/cparser/builder.h"
#include "beancount/ccore/number.h"
#include "beancount/ccore/date.h"

#include <iostream>
#include <list>
#include <string>
#include <unordered_map>
#include <utility>

#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "absl/strings/escaping.h"
#include "reflex/input.h"

namespace beancount {
using std::string;

namespace scanner {

// An extension to the generated scanner with some extra whizzbang.
class ScannerWithFile : public scanner::Scanner {
public:
  using scanner::Scanner::Scanner;

  void SetLineOffset(const int line_offset) {
    line_offset_ = line_offset;
  }

private:
  int line_offset_ = 0;
};

}  // namespace scanner
namespace parser {

}  // namespace parser
}  // namespace beancount
}  // %code top

// Inserted in implementation before including parser header
// (Within bison's parse() we should invoke lexer.lex(), not the global yylex().)
%code {
#undef yylex
#define yylex scanner.lex
}

//- Tokens --------------------------------------------------------------------
// Note: Token type names appear in the parser header; qualify fully.

/* Collection of value types. */
%define api.value.type variant // See: "C++ Variants"
%define api.token.constructor
%define api.token.prefix {TOK_}

/* Special error token, redefined to carry an error message. */
%token <std::string> YYerror
%token <std::string> YYUNDEF

/* Types for terminal symbols */
%token INDENT     /* Initial indent IF at the beginning of a line */
%token DEDENT     /* Initial indent IF at the beginning of a line */
%token EOL        /* End-of-line */
%token PIPE       /* | */
%token ATAT       /* @@ */
%token AT         /* @ */
%token LCURLCURL  /* {{ */
%token RCURLCURL  /* }} */
%token LCURL      /* { */
%token RCURL      /* } */
%token COMMA      /* , */
%token TILDE      /* ~ */
%token HASH       /* # */
%token ASTERISK   /* * */
%token SLASH      /* / */
%token COLON      /* : */
%token PLUS       /* + */
%token MINUS      /* - */
%token LPAREN     /* ( */
%token RPAREN     /* ) */
%token <char> FLAG    /* Valid characters for flags */
%token TXN                 /* 'txn' keyword */
%token BALANCE             /* 'balance' keyword */
%token OPEN                /* 'open' keyword */
%token CLOSE               /* 'close' keyword */
%token COMMODITY           /* 'commodity' keyword */
%token PAD                 /* 'pad' keyword */
%token EVENT               /* 'event' keyword */
%token PRICE               /* 'price' keyword */
%token NOTE                /* 'note' keyword */
%token DOCUMENT            /* 'document' keyword */
%token QUERY               /* 'query' keyword */
%token CUSTOM              /* 'custom' keyword */
%token PUSHTAG             /* 'pushtag' keyword */
%token POPTAG              /* 'poptag' keyword */
%token PUSHMETA            /* 'pushmeta' keyword */
%token POPMETA             /* 'popmeta' keyword */
%token OPTION              /* 'option' keyword */
%token OPTIONS             /* 'options' keyword */
%token INCLUDE             /* 'include' keyword */
%token PLUGIN              /* 'plugin' keyword */
%token NONE        /* A None value (parsed as NULL) */
%token <bool> BOOL        /* A boolean, true or false */
%token <absl::CivilDay> DATE        /* A date object */
%token <absl::CivilSecond> DATETIME        /* A date + time object */
%token <decimal::Decimal> NUMBER      /* A decimal number */
%token <std::string> ACCOUNT     /* The name of an account */
%token <std::string> CURRENCY    /* A currency specification */
%token <std::string> STRING      /* A quoted string, with any characters inside */
%token <std::string> TAG         /* A tag that can be associated with a transaction */
%token <std::string> LINK        /* A link that can be associated with a transaction */
%token <std::string> KEY         /* A key in a key-value pair */

/* Types for non-terminal symbols. */
%type <char> txn
%type <char> optflag
%type <std::string> account

%type <beancount::Directive*> transaction_line

%type <std::vector<beancount::Posting*>> posting_list
%type <beancount::Posting*> posting_and_metadata
%type <beancount::Posting*> posting

%type <beancount::Meta*> indented_metadata
%type <beancount::Meta*> metadata
%type <beancount::Meta::KV*> metadata_line
%type <beancount::MetaValue*> simple_value
%type <beancount::MetaValue*> meta_value
%type <std::vector<beancount::MetaValue*>> meta_value_list

/* Note: You could add constness aggressively. This wouldn't make much difference. */
%type <std::vector<std::string>> currency_list
%type <std::pair<beancount::Amount*, absl::optional<decimal::Decimal>>> amount_with_tolerance
%type <beancount::Amount*> amount
%type <beancount::inter::Expr*> expr
%type <beancount::inter::Expr*> maybe_expr
%type <absl::optional<std::string>> maybe_currency
%type <beancount::inter::PriceSpec*> price_annotation
%type <beancount::inter::CostSpec*> compound_expr
%type <beancount::inter::CostSpec*> compound_amount
%type <beancount::inter::CostSpec*> cost_comp
%type <beancount::inter::CostSpec*> cost_comp_list
%type <beancount::inter::CostSpec*> cost_spec

%type <beancount::Directive*> directive
%type <beancount::Directive*> transaction
%type <beancount::Directive*> balance
%type <beancount::Directive*> open
%type <beancount::Directive*> close
%type <beancount::Directive*> pad
%type <beancount::Directive*> document
%type <beancount::Directive*> note
%type <beancount::Directive*> event
%type <beancount::Directive*> price
%type <beancount::Directive*> commodity
%type <beancount::Directive*> query
%type <beancount::Directive*> custom

%type <std::pair<std::string, std::string>> payee_narration
%type <beancount::parser::TagsLinks*> tags_links
%type <std::string> filename
%type <options::Booking> booking

%type option
%type include
%type plugin

%type pushtag
%type poptag
%type pushmeta
%type popmeta

%type file
%type declarations

/* Operator precedence.
 * This is pulled straight out of the textbook example:
 * https://www.gnu.org/software/bison/manual/html_node/Infix-Calc.html#Infix-Calc
 */
%left MINUS PLUS
%left ASTERISK SLASH
%precedence NEGATIVE /* negation--unary minus */

/* Start symbol. */
%start file

/* We have some number of expected shift/reduce conflicts at 'eol'. */
/* NOTE(blais): We should have another go at taking that to zero. */
%expect 16

/* We don't free up memory for discarded tokens using %destructor (see
 * https://www.gnu.org/software/bison/manual/html_node/Destructor-Decl.html for
 * examples) in case of shifting during error recovery. The reason for this is
 * that in the parser below we carefully and explicitly deallocated temporaries
 * and sometimes use protocol buffer's ability to be set submessages that are
 * already allocated. See set_allocated_XXX() methods and AddAllocated() for
 * examples. */


/*- Grammar rules -----------------------------------------------------------*/
%%

/* Start token for parsing an entire file of the DSL. */
file:
  declarations YYEOF
    {
      builder.Finalize(location());
    }

/* End-of-line marker. In Beancount whitespace matters, so we don't swallow these and
   match them explicitly. */
eol: EOL

/* A rule to reduce pragmas and directives. We also handle errors at this
   level. */
declarations:
  declarations EOL
  /* Accumulate nothing for anything that's not a directive. The side-effects of
     all those pragma are stored in the builder object. */
  | declarations pragma
  /* A regular directive. Accmulate it in the builder. */
  | declarations directive
    {
      // Note: the reference is given to the builder.
      builder.AppendDirective($directive);

      // Set the location.
      auto* loc = $directive->mutable_location();
      loc->set_filename(scanner.filename);
      loc->set_lineno(@2.begin.line);
      loc->set_lineno_end(@2.end.line);
    }
  /* An error raised at the parser level. */
  | declarations error
    {
      // "Bison can force the situation to fit the rule, by discarding part of
      // the semantic context and part of the input. First it discards states
      // and objects from the stack until it gets back to a state in which the
      // error token is acceptable. (This means that the subexpressions already
      // parsed are discarded, back to the last complete stmts.) At this point
      // the error token can be shifted. Then, if the old lookahead token is not
      // acceptable to be shifted next, the parser reads tokens and discards
      // them until it finds a token which is acceptable. In this example, Bison
      // reads and discards input until the next newline so that the fourth rule
      // can apply. Note that discarded symbols are possible sources of memory
      // leaks, see Freeing Discarded Symbols, for a means to reclaim this
      // memory."
      //
      // Also, this could be a place to log an error in the builder's output.
    }
  | %empty

/*- Pragmas -----------------------------------------------------------------*/

/* pragmas: Anything but a directive. Those constructs apply side-effects to the
   builder object. */
pragma:
  pushtag
  | poptag
  | pushmeta
  | popmeta
  | option
  | include
  | plugin

/* A pragma that pushes a new tag applied to transactions. */
pushtag:
  PUSHTAG TAG eol
    {
      builder.PushTag($2);
    }

/* Remove a specific tag from the active list. */
poptag:
  POPTAG TAG eol
    {
      builder.PopTag($2, @$);
    }

/* A pragma that pushes a new key-value pair applied to transactions. */
pushmeta:
  PUSHMETA KEY COLON meta_value eol
    {
      builder.PushMeta($2, $4);
    }

/* Remove a specific key-value pairfrom the active list. */
popmeta:
  POPMETA KEY COLON eol
    {
      builder.PopMeta($2, @$);
    }

/* An option pragma. */
option:
  option_unary
  | option_binary

option_unary:
  OPTIONS STRING[proto] eol
    {
      builder.AddOptionUnary($proto, @$);
    }

option_binary:
  OPTION STRING[key] STRING[value] eol
    {
      builder.AddOptionBinary($key, std::move($value), @$);
    }

/* An include pragma. */
include:
  INCLUDE STRING eol
    {
      builder.AddInclude(std::move($2));
    }

/* A plugin pragma, along with its optional configuration. */
plugin:
  PLUGIN STRING eol
    {
      builder.AddPlugin(std::move($2), {});
    }
  | PLUGIN STRING STRING eol
    {
      builder.AddPlugin(std::move($2), $3);
    }

/*- Directives --------------------------------------------------------------*/

/* A rule to reduce any of the Beancount directives we offer. */
directive:
  transaction
  | price
  | balance
  | open
  | close
  | commodity
  | pad
  | document
  | note
  | event
  | query
  | custom

/* A Transaction directive, including postings. */
transaction:
  /* A transaction with no indented body (this is valid). */
  transaction_line
    {
      $$ = $1;
    }
  /* A transaction with an indented body. */
  | transaction_line INDENT metadata posting_list DEDENT
    {
      $$ = $1;

      // Insert the local metadata on top of that from the directive.
      // {99ef4ca51cd5}
      if ($metadata != nullptr) {
        $$->mutable_meta()->MergeFrom(*$metadata);
        delete $metadata;
      }

      // Attach postings to the directive.
      auto* txn = $$->mutable_transaction();
      for (const auto* posting : $posting_list) {
        txn->mutable_postings()->Add()->CopyFrom(*posting);
        delete posting;
      }
      $posting_list.clear();
    }

/* Matches just the first line of a transaction, without its metadata. */
transaction_line:
  DATE txn[flag] payee_narration[pn] tags_links eol
    {
      // Create directive and update head, without custom metadata. That is
      // added at {99ef4ca51cd5}.
      $$ = builder.MakeDirective($1, nullptr, &$tags_links);

      // Create Transaction and update flag.
      auto* txn = $$->mutable_transaction();
      if ($flag != '\0') {
        txn->set_flag(&$flag, 1);
      }

      // Update payee and narration fields.
      if (!$pn.first.empty()) {
        txn->set_payee($pn.first);
      }
      if (!$pn.second.empty()) {
        txn->set_narration($pn.second);
      }
    }

/* A Price directive. */
price:
  DATE PRICE CURRENCY amount tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* price = $$->mutable_price();
      price->set_currency(std::move($3));
      price->set_allocated_amount($amount);
    }

/* A Balance directive. */
balance:
  DATE BALANCE account amount_with_tolerance[atol] tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* balance = $$->mutable_balance();
      balance->set_account($account);
      balance->set_allocated_amount($atol.first);
      if ($atol.second.has_value()) {
        DecimalToProto($atol.second.value(),
                       builder.decimal_use_triple(),
                       balance->mutable_tolerance());
      }
      // Note: We leave `difference` unset.
    }

/* An Open directive. */
open:
  DATE OPEN account currency_list booking tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* open = $$->mutable_open();
      open->set_account($account);

      for (const auto& currency : $currency_list) {
        open->add_currencies(currency);
      }

      if ($booking != options::Booking::UNKNOWN) {
        open->set_booking($booking);
      }
    }

/* A Close directive. */
close:
  DATE CLOSE account tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* open = $$->mutable_close();
      open->set_account($account);
    }

/* A Commodity directive. */
commodity:
  DATE COMMODITY CURRENCY tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* commodity = $$->mutable_commodity();
      commodity->set_currency(std::move($CURRENCY));
    }

/* A Pad directive. */
pad:
  DATE PAD account account[source] tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* pad = $$->mutable_pad();
      pad->set_account($3);
      pad->set_source_account($source);
    }

/* A Document directive. */
document:
  DATE DOCUMENT account filename tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* document = $$->mutable_document();
      document->set_account($account);

      string abs_filename = builder.MakeAbsolutePath($filename);
      document->set_filename(abs_filename);
    }

/* A Note directive. */
note:
  DATE NOTE account STRING[comment] tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* note = $$->mutable_note();
      note->set_account($3);
      note->set_comment(std::move($comment));
    }

/* An Event directive. */
event:
  DATE EVENT STRING[type] STRING[description] tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* event = $$->mutable_event();
      event->set_type(std::move($type));
      event->set_description(std::move($description));
    }

/* A Query directive. */
query:
  DATE QUERY STRING[name] STRING[qstr] tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* query = $$->mutable_query();
      query->set_name(std::move($name));
      query->set_query_string(std::move($qstr));
    }

/* A Custom directive. Note that this directive does not support the dedicated
   tags and links because those can be part of metadata. It would be ambiguous
   which they are. */
custom:
  DATE CUSTOM STRING[type] meta_value_list eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, nullptr);
      auto* custom = $$->mutable_custom();
      custom->set_type(std::move($type));

      // Copy custom values here.
      auto* values = custom->mutable_values();
      for (auto* meta_value : $meta_value_list) {
        values->AddAllocated(meta_value);
      }
      $meta_value_list.clear();
    }

/*- Metadata ----------------------------------------------------------------*/

/* A list of metadata (optionally empty), including its indentation. */
indented_metadata:
  %empty
    {
      $$ = nullptr;
    }
  | INDENT metadata DEDENT
    {
      $$ = $2;
      if ($$ != nullptr) {
      }
    }

/* A list of metadata (optionally empty). */
metadata:
  %empty
    {
      $$ = nullptr;
    }
  | metadata metadata_line
    {
      if ($1 == nullptr) {
        $$ = new beancount::Meta();
      } else {
        $$ = $1;
      }
      auto* kv = $$->add_kv();
      kv->CopyFrom(*$2);
      delete $2;
    }

/* A single line of a metadata declaration. */
metadata_line:
  /* Regular key-value pairs. */
  KEY COLON meta_value eol
    {
      $$ = new beancount::Meta::KV();
      $$->set_key($1);
      $$->mutable_value()->CopyFrom(*$3);
      delete $3;
    }
  /* A tag can be used by itself as metadata. */
  | TAG eol
    {
      $$ = new beancount::Meta::KV();
      $$->mutable_value()->set_tag($1);
    }
  /* A link can be used by itself as metadata. */
  | LINK eol
    {
      $$ = new beancount::Meta::KV();
      $$->mutable_value()->set_link($1);
    }

/* A general variant that can contain basic types, used as the value in
   key-value pairs of metadata and in Custom directives. */
simple_value:
  STRING
    {
      $$ = new beancount::MetaValue();
      $$->set_text(std::move($1));
    }
  | CURRENCY
    {
      $$ = new beancount::MetaValue();
      $$->set_currency(std::move($1));
    }
  | account
    {
      $$ = new beancount::MetaValue();
      $$->set_account($1);
    }
  | TAG
    {
      $$ = new beancount::MetaValue();
      $$->set_tag($1);
    }
  | LINK
    {
      $$ = new beancount::MetaValue();
      $$->set_link($1);
    }
  | DATE
    {
      $$ = new beancount::MetaValue();
      DateToProto($1, $$->mutable_date());
    }
  | BOOL
    {
      $$ = new beancount::MetaValue();
      $$->set_boolean($1);
    }
  | NONE
    {
      $$ = new beancount::MetaValue();
      /* NULL is a proto not set */
    }
  | expr
    {
      // Evaluate the expression here and now. Note that this is an exception to
      // the rest of the evaluations and is performed in the parser using the
      // default context, as it would be awkward to define a temporary holding
      // Expr in the one-of field for metavalues. This is going to be rare, and
      // the effect subtle, we could eventually delay this evaluation if desired
      // by some users. For now we keep things simple by evaluating here and
      // now.
      $$ = new beancount::MetaValue();
      auto num = EvaluateExpression(*$expr, decimal::context);
      DecimalToProto(num, builder.decimal_use_triple(), $$->mutable_number());
      delete $expr;
    }
  | %empty
    {
      $$ = new beancount::MetaValue();
    }

/* Metadata value, with added possibility of amount. */
meta_value:
  simple_value
  | amount
    {
      $$ = new beancount::MetaValue();
      $$->set_allocated_amount($1);
    }

/* A list of variant values. */
meta_value_list:
  %empty
    {
      $$ = {};
    }
  | meta_value_list simple_value
    {
      $$ = $1;
      $$.push_back($2);
    }

/* A container for tags and links, which show up at the end of a transaction's
   first line or in some of the directives. */
tags_links:
  %empty
    {
      $$ = nullptr;
    }
  | tags_links TAG
    {
      $$ = ($1 != nullptr) ? ($1) : new TagsLinks{};
      $$->tags.push_back($2);
    }
  | tags_links LINK
    {
      $$ = ($1 != nullptr) ? ($1) : new TagsLinks{};
      $$->links.push_back($2);
    }

/*- String matchers ---------------------------------------------------------*/

/* Payee and narration strings. */
payee_narration:
  /* Variant when both strings are present. */
  STRING STRING
    {
      $$ = std::make_pair(std::move($1), std::move($2));
    }
  /* Variant when only one string is present; interpret as narration only. */
  | STRING
    {
      $$ = std::make_pair("", std::move($1));
    }
  | %empty
    {
      $$ = std::make_pair("", "");
    }

/* An  account name. */
account:
  ACCOUNT
    {
      $$ = builder.InternAccount(std::move($1), @$);
    }

/* A filename. Consider specializing this. */
filename:
  STRING

/* A comma-separated list of currencies. */
currency_list:
  %empty
    {
      $$ = {};
    }
  | CURRENCY
    {
      $$.push_back(std::move($1));
    }
  | currency_list COMMA CURRENCY
    {
      $$ = $1;
      $$.push_back(std::move($3));
    }

/*- Arithmetic expressions --------------------------------------------------*/

/* Arithmetic expression, delayed for later evaluation. */
expr:
  NUMBER
    {
      auto* expr = $$ = new inter::Expr();
      expr->set_op(inter::ExprOp::NUM);
      DecimalToProto($1, builder.decimal_use_triple(), expr->mutable_number());
    }
  | expr PLUS expr
    {
      auto* expr = $$ = new inter::Expr();
      expr->set_op(inter::ExprOp::ADD);
      expr->set_allocated_arg1($1);
      expr->set_allocated_arg2($3);
    }
  | expr MINUS expr
    {
      auto* expr = $$ = new inter::Expr();
      expr->set_op(inter::ExprOp::SUB);
      expr->set_allocated_arg1($1);
      expr->set_allocated_arg2($3);
    }
  | expr ASTERISK expr
    {
      auto* expr = $$ = new inter::Expr();
      expr->set_op(inter::ExprOp::MUL);
      expr->set_allocated_arg1($1);
      expr->set_allocated_arg2($3);
    }
  | expr SLASH expr
    {
      auto* expr = $$ = new inter::Expr();
      expr->set_op(inter::ExprOp::DIV);
      expr->set_allocated_arg1($1);
      expr->set_allocated_arg2($3);
    }
  | MINUS expr %prec NEGATIVE
    {
      auto* expr = $$ = new inter::Expr();
      expr->set_op(inter::ExprOp::NEG);
      expr->set_allocated_arg1($2);
    }
  | PLUS expr %prec NEGATIVE
    {
      auto* expr = $$ = new inter::Expr();
      expr->set_op(inter::ExprOp::PLUS);
      expr->set_allocated_arg1($2);
    }
  | LPAREN expr RPAREN
    {
      auto* expr = $$ = new inter::Expr();
      expr->set_op(inter::ExprOp::PAREN);
      expr->set_allocated_arg1($2);
    }

/* Expression with immediate evaluation. */

/*- Single-character flags & other constants --------------------------------*/

/* The flag in a transaction declaration: it can be either 'txn' or one of the
   special character flags. */
txn:
  TXN
    {
      $$ = '*';
    }
  | FLAG
    {
        $$ = $1;
    }
  | ASTERISK
    {
        $$ = '*';
    }
  | HASH
    {
        $$ = '#';
    }

/* An optional flag marking a posting. */
optflag:
  %empty
    {
      $$ = '\0';
    }
  | ASTERISK
    {
      $$ = '*';
    }
  | HASH
    {
      $$ = '#';
    }
  | FLAG

/* The declaration of a booking method as a string. */
booking:
  STRING
    {
      options::Booking method;
      if (!Booking_Parse($1, &method)) {
        builder.AddError(absl::StrFormat("Invalid value for booking method: '%s'", $1), @$);
        method = options::Booking::UNKNOWN;
      }
      $$ = method;
    }
  | %empty
    {
      $$ = options::Booking::UNKNOWN;
    }

/*- Postings ----------------------------------------------------------------*/

/* A list of postings, possibly empty. */
posting_list:
  %empty
    {
      $$ = {};
    }
  /* Matches an empty or comment indented line. It's convenient to be able to
   * comment indented postings. */
  | posting_list eol
  /* Matches an actual posting. */
  | posting_list posting_and_metadata
    {
      $1.push_back($2);
      $$ = $1;
    }

/* A single posting and its associated metadata. */
posting_and_metadata:
  posting indented_metadata
    {
      if ($2 != nullptr) {
        $1->mutable_meta()->CopyFrom(*$2);
        delete $2;
      }
      $$ = $posting;

      // Set the location.
      auto* loc = $posting->mutable_location();
      loc->set_filename(scanner.filename);
      loc->set_lineno(@1.begin.line);
      loc->set_lineno_end(@2.end.line);
    }

/* A posting line, without any of its metadata. */
posting:
  /* Variation with no price. */
  optflag account maybe_expr maybe_currency cost_spec eol
    {
      $$ = new Posting();
      if ($cost_spec != nullptr) {
        $$->mutable_spec()->mutable_cost()->CopyFrom(*$cost_spec);
      }
      builder.PreparePosting($$, $maybe_expr, $maybe_currency, $optflag, $account, false, @$);
      delete $maybe_expr;
    }
  /* Variation with price. */
  | optflag account maybe_expr maybe_currency cost_spec AT price_annotation eol
    {
      $$ = new Posting();
      if ($cost_spec != nullptr) {
        $$->mutable_spec()->mutable_cost()->CopyFrom(*$cost_spec);
      }
      $$->mutable_spec()->mutable_price()->CopyFrom(*$price_annotation);
      builder.PreparePosting($$, $maybe_expr, $maybe_currency, $optflag, $account, false, @$);
      delete $maybe_expr;
      delete $price_annotation;
    }
  /* Variation with complete price. */
  | optflag account maybe_expr maybe_currency cost_spec ATAT price_annotation eol
    {
      $$ = new Posting();
      if ($cost_spec != nullptr) {
        $$->mutable_spec()->mutable_cost()->CopyFrom(*$cost_spec);
      }
      auto* price_spec = $$->mutable_spec()->mutable_price();
      price_spec->CopyFrom(*$price_annotation);
      price_spec->set_is_total(true);
      builder.PreparePosting($$, $maybe_expr, $maybe_currency, $optflag, $account, true, @$);
      delete $maybe_expr;
      delete $price_annotation;
    }
  /* Variation with just an account name. */
  | optflag account eol
    {
      $$ = new Posting();
      builder.PreparePosting($$, {}, {}, $optflag, $account, false, @$);
    }

/* A price annotation on a posting. */
price_annotation:
  maybe_expr maybe_currency
    {
      auto* spec = $$ = new inter::PriceSpec();
      if ($maybe_expr) {
        SetExprOrNumber(spec, *$maybe_expr);
        delete $maybe_expr;
      }

      if ($maybe_currency.has_value()) {
        spec->set_currency($maybe_currency.value());
      }
    }

/*- Amounts & Costs ---------------------------------------------------------*/

/* An Amount, which is a decimal number and a currency. */
amount:
  expr CURRENCY
    {
      auto* amount = $$ = new Amount();
      SetExprOrNumber(amount, *$expr);
      delete $expr;
      amount->set_currency(std::move($2));
    }

/* An amount, with an optional associated tolerance. */
amount_with_tolerance:
  amount
    {
      $$ = make_pair($1, absl::optional<decimal::Decimal>{});
    }
  | expr TILDE NUMBER[tolerance] CURRENCY
    {
      // Note: We do not support an expression for the tolerance value anymore.
      // This would likely unnecesasry anyway and keeps the definitions upstream
      // simpler.
      auto* amount = new Amount();
      SetExprOrNumber(amount, *$expr);
      delete $expr;
      amount->set_currency(std::move($4));
      $$ = std::make_pair(amount, $tolerance);
    }

/* An optional expression. */
maybe_expr:
  expr
  | %empty
    {
      $$ = nullptr;
    }

/* An optional currency. */
maybe_currency:
  CURRENCY
    {
      $$ = {$1};
    }
  | %empty
    {
      $$ = {};
    }

/* A per-unit/total amount specification, to be used in cost specification. */
compound_amount:
  CURRENCY
    {
      $$ = new inter::CostSpec();
      $$->set_currency(std::move($1));
    }
  | compound_expr
    {
      $$ = $1;
    }
  | compound_expr CURRENCY
    {
      $$ = $1;
      $$->set_currency(std::move($2));
    }

/* An expression or an optioanl pair of them separated by a #. */
compound_expr:
  expr[per_unit]
    {
      $$ = new inter::CostSpec();

      // Set per-unit field.
      auto* spec = $$->mutable_per_unit();
      SetExprOrNumber(spec, *$per_unit);
      delete $per_unit;
    }
  | maybe_expr[per_unit] HASH maybe_expr[total]
    {
      $$ = new inter::CostSpec();

      if ($per_unit) {
        // Set per-unit field.
        auto* spec = $$->mutable_per_unit();
        SetExprOrNumber(spec, *$per_unit);
        delete $per_unit;
      }

      if ($total) {
        // Set total field.
        auto* spec = $$->mutable_total();
        SetExprOrNumber(spec, *$total);
        delete $total;
      }
    }

/* A cost specification. */
cost_spec:
  LCURL cost_comp_list RCURL
    {
      $$ = $2;
    }
  | LCURLCURL cost_comp_list RCURLCURL
    {
      // Note: This is using the old special syntax for total for: {{...}}
      // It is an error to set the total to a value in the input when using this
      // syntax.
      if ($2->has_total()) {
        builder.AddError(
            absl::StrFormat(
                "Per-unit cost may not be specified using total cost "
                "syntax: '%s'; ignoring per-unit cost", $2->DebugString()), @$);
      } else {
        $2->mutable_total()->CopyFrom($2->per_unit());
      }
      $2->clear_per_unit();
      $$ = $2;
    }
  | %empty
    {
      $$ = nullptr;
    }

/* A comma-separated list of components in a {...} cost specification. */
cost_comp_list:
  %empty
    {
      $$ = new inter::CostSpec();
    }
  | cost_comp
  | cost_comp_list COMMA cost_comp
    {
      $$ = $1;
      auto status = builder.MergeCost(*$3, $$);
      delete $3;
      if (!status.ok()) {
        builder.AddError(status.message(), @3);
        YYERROR;
      }
    }

/* One component of a cost specification. */
cost_comp:
  compound_amount
  | DATE
    {
      $$ = new inter::CostSpec();
      DateToProto($1, $$->mutable_date());
    }
  | STRING
    {
      $$ = new inter::CostSpec();
      $$->set_label(std::move($1));
    }
  | ASTERISK
    {
      $$ = new inter::CostSpec();
      $$->set_merge_cost(true);
    }

%%
//- User code ----------------------------------------------------------------

// On a scanner exception, Parser::syntax_error() is thrown and caught by the
// parser source code and this method is called with it.
void beancount::parser::Parser::error(const location& loc, const string& msg)
{
  // Log an error.
  builder.AddError(msg, loc);

  // If token is unknown (no match).
  if (scanner.size() == 0) {
    scanner.matcher().winput(); // Skip character.
  }
}

namespace beancount {
namespace parser {
using std::string_view;

std::unique_ptr<Ledger> Parse(
    const reflex::Input& input,
    const absl::optional<string_view>& filename = {},
    int line_offset = 0,
    bool debug = false,
    bool decimal_use_triple = false) {

  // Create a scanner and builder.
  scanner::ScannerWithFile scanner(input, std::cout);
  if (filename.has_value()) {
    scanner.filename = filename.value();
  }
  scanner.SetLineOffset(line_offset);
  parser::Builder builder(scanner);
  builder.SetDecimalUseTriple(decimal_use_triple);

  // Check for invalid input before starting.
  if (!input.eof() && !input.good()) {
    location loc;
    builder.AddError("An IO error has occurred.", loc);
    return builder.MakeLedger();
  }

  // Run the parser.
  beancount::parser::Parser parser(scanner, builder);
  parser.set_debug_stream(std::cerr);
  if (debug) {
    parser.set_debug_level(1);
    scanner.set_debug(1);
  }
  int result;
  string error_message;
  try {
    result = parser.parse();
  } catch (const std::runtime_error& exc) {
    result = -1000;
    error_message = exc.what();
  }

  // Handle error case.
  auto ledger = builder.MakeLedger();
  if (result != 0) {
    std::ostringstream oss;
    oss << "Error parsing ledger with result: " << result;
    if (result == -1000) {
      oss << " (" << error_message << ")";
    }
    Location location;
    if (filename.has_value()) {
      location.set_filename(string(filename.value()));
    }
    AddError(ledger.get(), oss.str(), location);
  }

  if (debug) {
    // This is a noop, unless %option perf-report is enabled in the scanner.
    scanner.perf_report();
  }

  return ledger;
}

std::unique_ptr<Ledger> ParseFile(string_view filename) {
  std::ifstream in = std::ifstream(string(filename), std::ios::in);
  reflex::Input input(in);
  return parser::Parse(input, filename, 0);
}

std::unique_ptr<Ledger> ParseString(string_view input_string,
                                    string_view filename,
                                    int line_offset,
                                    bool debug,
                                    bool decimal_use_triple) {
  // Ensure the string is converted to UTF8 by using reflex::Input and
  // instantiate a scanner.
  reflex::Input input(input_string.data(), input_string.size());
  return parser::Parse(input, filename, line_offset, debug, decimal_use_triple);
}

}  // namespace parser
}  // namespace beancount
